---
title: 네트워크요청 디바운싱/취소 (Debouncing/Cancelling)
version: 1
---

import { Link } from "/src/components/Link";
import { AutoSnippet, When } from "/src/components/CodeSnippet";
import homeScreen from "!raw-loader!/docs/case_studies/cancel/home_screen.dart";
import extension from "!raw-loader!/docs/case_studies/cancel/extension.dart";
import detailScreen from "/docs/case_studies/cancel/detail_screen";
import detailScreenCancel from "/docs/case_studies/cancel/detail_screen_cancel";
import detailScreenDebounce from "/docs/case_studies/cancel/detail_screen_debounce";
import providerWithExtension from "/docs/case_studies/cancel/provider_with_extension";

애플리케이션이 복잡해짐에 따라 동시에 여러 개의 네트워크 요청이 발생하는 것이 일반적입니다. 
예를 들어, 사용자가 검색창에 입력할 때마다 새로운 요청이 트리거될 수 있습니다. 
사용자가 빠르게 입력하는 경우 애플리케이션에 동시에 많은 요청이 전송될 수 있습니다.

또는 사용자가 요청을 트리거한 후 요청이 완료되기 전에 다른 페이지로 이동할 수도 있습니다. 
이 경우 애플리케이션에 더 이상 필요하지 않은 요청이 전송 중일 수 있습니다.

이러한 상황에서 성능을 최적화하기 위해 사용할 수 있는 몇 가지 기술이 있습니다:

- 요청 '디바운스'. 
  즉, 사용자가 일정 시간 동안 입력을 멈출 때까지 기다렸다가 요청을 전송하는 방식입니다. 
  이렇게 하면 사용자가 빠르게 입력하더라도 주어진 입력에 대해 한 번의 요청만 전송할 수 있습니다.
- 요청 '취소'. 
  즉, 요청이 완료되기 전에 사용자가 페이지에서 다른 곳으로 이동하는 경우 요청을 취소합니다. 
  이렇게 하면 사용자가 볼 수 없는 응답을 처리하느라 시간을 낭비하지 않아도 됩니다.

Riverpod에서는 이 두 가지 기술을 비슷한 방식으로 구현할 수 있습니다.
핵심은 `ref.onDispose`를 "자동 폐기(automatic disposal)" 또는 `ref.watch`와 함께 사용하여 원하는 동작을 달성하는 것입니다.

이를 보여주기 위해 두 페이지로 구성된 간단한 애플리케이션을 만들어 보겠습니다:

- 새 페이지를 여는 버튼이 있는 홈 화면
- [Bored API](https://www.boredapi.com/)에서 임의의 액티비티를 표시하는 상세 페이지로, 액티비티를 새로 고칠 수 있는 기능이 있습니다.  
  당겨서 새로고침(pull to refresh)를 구현하는 방법에 대한 자세한 내용은 <Link documentID="case_studies/pull_to_refresh" />를 참조하세요.

그런 다음 다음 동작을 구현합니다:

- 사용자가 세부 정보 페이지를 열었다가 즉시 다시 이동하면 액티비티에 대한 요청을 취소(cancel)합니다.
- 사용자가 연속으로 여러 번 액티비티을 새로 고치면 요청을 디바운스(debounce)하여 사용자가 새로 고침을 중지한 후 한 번만 요청을 보내도록 합니다.

## 어플리케이션

<img
  src="/img/case_studies/cancel/app.gif"
  alt="Gif는 어플리케이션을 보여주고, 상세 페이지를 열고, 액티비티를 새로 고침합니다."
/>

먼저, 디바운스나 취소 없이 애플리케이션을 만들어 봅시다.  
여기서는 멋진 것을 사용하지 않고, 세부 정보 페이지를 여는 `Navigator.push`가 있는 평범한 `FloatingActionButton`을 사용하겠습니다.

먼저 홈 화면을 정의하는 것부터 시작하겠습니다. 
평소와 마찬가지로 애플리케이션의 루트에 `ProviderScope`를 지정하는 것을 잊지 마세요.

<AutoSnippet title="lib/src/main.dart" 
  raw={homeScreen} 
  translations={{
  }}
/>

그런 다음 세부 정보 페이지를 정의해 보겠습니다. 
활동을 가져오고 당겨서 새로고침(pull to refresh)를 구현하려면 <Link documentID="case_studies/pull_to_refresh" /> 사례 연구를 참조하세요.

<AutoSnippet title="lib/src/detail_screen.dart" 
  {...detailScreen} 
  translations={{
  }}
/>

## 요청 취소하기

이제 애플리케이션이 작동하므로 취소(cancellation) 로직을 구현해 보겠습니다.

이를 위해 사용자가 페이지에서 다른 곳으로 이동할 때 `ref.onDispose`를 사용하여 요청을 취소할 것입니다. 
이 기능이 작동하려면 providers의 자동 폐기(automatic disposal)가 활성화되어 있어야 합니다.

요청을 취소하는 데 필요한 정확한 코드는 HTTP 클라이언트에 따라 다릅니다.
이 예에서는 `package:http`를 사용하지만 다른 클라이언트에도 동일한 원칙이 적용됩니다.

여기서 중요한 점은 사용자가 다른 곳으로 이동할 때 `ref.onDispose`가 호출된다는 것입니다.
이는 provider가 더 이상 사용되지 않으므로 자동 폐기를 통해 폐기되기 때문입니다.  
따라서 이 콜백을 사용하여 요청을 취소할 수 있습니다. 
`package:http`를 사용하는 경우 HTTP 클라이언트를 닫으면 이 작업을 수행할 수 있습니다.

<AutoSnippet 
  {...detailScreenCancel} 
  translations={{
    client: "  // package:http를 사용하여 HTTP 클라이언트를 생성합니다.",
    onDispose: "  // 폐기 시 클라이언트를 닫습니다.\n  // 그러면 클라이언트에 있을 수 있는 모든 보류 중인 요청이 취소됩니다.",
    get: "  // 이제 'get' 함수 대신 클라이언트를 사용하여 요청을 수행합니다.",
    jsonDecode: "  // 나머지 코드는 이전과 동일합니다.",
  }}
/>

## 요청 디바운싱(Debouncing)

이제 취소를 구현했으니 이제 디바운싱을 구현해 보겠습니다.  
현재로서는 사용자가 활동을 연속으로 여러 번 새로 고치면 새로 고칠 때마다 요청을 보내게 됩니다.

기술적으로는 취소를 구현했으므로 문제가 되지 않습니다. 
사용자가 활동을 연속으로 여러 번 새로 고치면 새 요청이 이루어질 때 이전 요청이 취소됩니다.

하지만 이는 이상적이지 않습니다. 여전히 여러 요청을 전송하고 대역폭과 서버 리소스를 낭비하게 됩니다.  
대신 사용자가 일정 시간 동안 활동 새로 고침을 중지할 때까지 요청을 지연시키는 방법을 사용할 수 있습니다.

여기서 로직은 취소 로직과 매우 유사합니다. 다시 `ref.onDispose`를 사용합니다. 
하지만 여기서는 HTTP 클라이언트를 닫는 대신 `onDispose`에 의존하여 요청이 시작되기 전에 중단한다는 점이 다릅니다.  
그런 다음 요청을 보내기 전에 임의로 500ms를 기다립니다.
그런 다음 사용자가 500ms가 경과하기 전에 활동을 다시 새로고침하면 `onDispose`가 호출되어 요청이 중단됩니다.

:::info
요청을 중단하려면, 자발적(voluntarily)으로 던지는(throw) 것이 일반적입니다.  
provider가 폐기된(disposed) 후에는 providers 내부에 던져도(throw) 안전합니다.
예외는 당연히 Riverpod에 의해 잡히고 무시됩니다.
:::

<AutoSnippet 
  {...detailScreenDebounce} 
  translations={{
    didDispose: "  // provider가 현재 폐기되었는지 여부를 캡처합니다.",
    delayed: "  // 사용자가 새로 고침을 중단할 때까지 요청을 500밀리초 지연합니다.",
    cancelled: "  // 지연 중에 provider가 dispose되었다면, 사용자가 다시 새로고침했다는 의미입니다. \n  // 예외를 던져 요청을 취소합니다.\n  // Riverpod에 의해 포착되므로 예외를 사용하는 것이 안전합니다.",
    http: "  // 다음 코드는 이전 스니펫에서 변경되지 않았습니다.",
  }}
/>

## 더 나아가기: 두 가지를 한 번에 수행하기

이제 요청을 디바운스하고 취소하는 방법을 알았습니다.  
하지만 현재 다른 요청을 수행하려면 동일한 로직을 여러 곳에 복사하여 붙여넣어야 합니다. 이것은 이상적이지 않습니다.

하지만 여기서 더 나아가 재사용 가능한 유틸리티를 구현하여 두 가지 작업을 한 번에 수행할 수 있습니다.

여기서는 단일 메서드에서 취소와 디바운싱을 모두 처리하는 확장 메서드(extension method)를 `Ref`에 구현하는 것이 아이디어입니다.

<AutoSnippet 
  raw={extension} 
  translations={{
    note: "  /// [duration](기본값은 500ms) 동안 기다린 다음 요청에 사용할 수 있는 [http.Client]를 반환합니다.\n  ///\n  /// 해당 클라이언트는 provider가 폐기되면 자동으로 닫힙니다.",
    didDispose: "    // 먼저 디바운싱을 처리합니다.",
    delay: "    // 사용자가 새로 고침을 중단할 때까지 요청을 500밀리초 지연합니다.",
    cancel: "    // 지연 중에 provider가 dispose되었다면, 사용자가 다시 새로고침했다는 의미입니다.\n    // 예외를 던져 요청을 취소합니다.\n    // Riverpod에 의해 포착되므로 예외를 사용하는 것이 안전합니다.",
    client: "    // 이제 클라이언트를 생성하고 provider가 폐기되면 닫습니다.",
    return: "    // 마지막으로 클라이언트를 반환하여 provider가 요청을 할 수 있도록 합니다.",
  }}
/>

그런 다음 다음과 같이 providers에서 이 확장 메서드를 사용할 수 있습니다:

<AutoSnippet 
  {...providerWithExtension} 
  translations={{
    client: "  // 앞서 만든 확장자를 사용하여 HTTP 클라이언트를 가져옵니다.",
    get: "  // 이제 'get' 함수 대신 클라이언트를 사용하여 요청을 수행합니다.\n  // 사용자가 페이지를 떠나면 요청이 자연스럽게 디바운스되고 취소됩니다.",
  }}
/>
